package com.linciping.gradle.asm.transform

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.google.common.collect.ImmutableSet
import com.google.common.io.Files
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.codehaus.groovy.runtime.IOGroovyMethods
import org.objectweb.asm.*
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

class AsmSampleTransform : Transform() {
    /**
     * transform的名字
     */
    override fun getName(): String {
        return "AsmSample"
    }

    /**
     * 转换使用的数据类型
     * CLASSES 编译后的Java代码
     * RESOURCES 标准的Java 资源
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 作用域
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        //当前项目和依赖项目
        return ImmutableSet.of(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.SUB_PROJECTS)
    }

    /**
     * 是否增量更新
     */
    override fun isIncremental(): Boolean {
        return true
    }

    /**
     * 处理transform
     */
    override fun transform(transformInvocation: TransformInvocation?) {
        if (transformInvocation == null) return
        val transformationContext = transformInvocation.context
        val transformInput = transformInvocation.inputs
        val transformOutputProvider = transformInvocation.outputProvider
        val isIncremental = transformInvocation.isIncremental
        if (!isIncremental) {
            //如果不是增量更新，就清空输出区
            transformOutputProvider.deleteAll()
        }
        //遍历所有的是输入内容
        transformInput.forEach { input ->
            //读取当前项目的源码目录
            input.directoryInputs.forEach { directoryInput ->
                val dest = transformOutputProvider.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY
                )
                val inputDir = directoryInput.file
                if (isIncremental) {
                    directoryInput.changedFiles.forEach { (inputFile, status) ->
                        when (status!!) {
                            Status.NOTCHANGED -> {
                            }
                            Status.ADDED,
                            Status.CHANGED -> {
                                transformSingleFile(inputDir, inputFile, dest, transformationContext.temporaryDir)
                            }
                            Status.REMOVED -> {
                                if (dest.exists()) {
                                    FileUtils.forceDelete(dest)
                                }
                            }
                        }
                    }
                } else {
                    foreachInputDir(inputDir, dest, transformationContext.temporaryDir)
                }
            }
            //读取依赖目录
            input.jarInputs.forEach { jarInput ->
                val status = jarInput.status
                //如果是增量就根据状态来处理jar文件
                //如果不是就处理全部
                if (isIncremental) {
                    when (status!!) {
                        Status.NOTCHANGED -> {
                        }
                        Status.ADDED,
                        Status.CHANGED
                        -> {
                            transformJar(jarInput, transformOutputProvider, transformationContext.temporaryDir)
                        }
                        Status.REMOVED -> {
                            if (jarInput.file.exists()) {
                                FileUtils.forceDelete(jarInput.file)
                            }
                        }
                    }
                } else {
                    transformJar(jarInput, transformOutputProvider, transformationContext.temporaryDir)
                }
            }
        }
    }

    /**
     * 处理jar
     */
    private fun transformJar(jarInput: JarInput, transformOutputProvider: TransformOutputProvider, temporaryDir: File) {
        var jarName = jarInput.file.name
        val hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
        if (jarName.endsWith(".jar")) {
            jarName = jarName.substring(0, jarName.length - 4)
        }
        val realJarName = "${jarName}_$hexName"
        val dest =
            transformOutputProvider.getContentLocation(realJarName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
        var changeJar = foreachJar(jarInput.file, realJarName, temporaryDir)
        if (changeJar == null) {
            changeJar = jarInput.file
        }
        FileUtils.copyFile(changeJar, dest)
    }

    /**
     * 遍历jar的所有文件
     */
    private fun foreachJar(file: File, outputJarName: String, tempDir: File): File? {
        val outputJar = File(tempDir, "$outputJarName.jar")
        val jarOutputStream = JarOutputStream(FileOutputStream(outputJar))
        val jarFile = JarFile(file)
        val enumeration = jarFile.entries()
        while (enumeration.hasMoreElements()) {
            val jarEntry = enumeration.nextElement()
            val jarInputStream = jarFile.getInputStream(jarEntry)
            val jarEntryName = jarEntry.name
            val zipEntry = ZipEntry(jarEntryName)
            jarOutputStream.putNextEntry(zipEntry)
            val classBytes = IOUtils.toByteArray(jarInputStream)
            var transformClassBytes: ByteArray? = null
            if (jarEntryName.endsWith(".class")) {
                transformClassBytes = transformJarClass(jarEntryName, classBytes)
            }
            if (transformClassBytes == null) {
                jarOutputStream.write(classBytes)
            } else {
                jarOutputStream.write(transformClassBytes)
            }
            jarOutputStream.closeEntry()
        }
        jarOutputStream.close()
        jarFile.close()
        return outputJar
    }

    /**
     * 单个文件 transform
     */
    private fun transformSingleFile(inputDir: File, inputFile: File, dest: File, tempDir: File) {
        try {
            FileUtils.touch(dest)
        } catch (ex: Exception) {
            Files.createParentDirs(dest)
        }
        val changeFile = transformClass(inputFile, tempDir)
        if (changeFile != null) {
            val path = changeFile.absolutePath.replace(inputDir.absolutePath, "")
            val target = File(dest.absolutePath + path)
            if (target.exists()) {
                FileUtils.forceDelete(target)
            }
            FileUtils.copyFile(inputFile, target)
            inputFile.delete()
        }
    }

    /**
     * 遍历整个文件夹，并transform
     */
    private fun foreachInputDir(inputDir: File, dest: File, tempDir: File) {
        val changeFileMap: MutableMap<String, File> = hashMapOf()
        fileTraverse(inputDir, { _, name -> name.endsWith(".class") }, { classFile ->
            val changeFile = transformClass(classFile, tempDir)
            if (changeFile != null) {
                val key = classFile.absolutePath.replace(inputDir.absolutePath, "")
                changeFileMap[key] = changeFile
            }
        })
        FileUtils.copyDirectory(inputDir, dest)
        changeFileMap.forEach { (path, file) ->
            val target = File(dest.absolutePath + path)
            if (target.exists()) {
                FileUtils.forceDelete(target)
            }
            FileUtils.copyFile(file, target)
            file.delete()
        }
    }

    /**
     * transform jar class
     */
    private fun transformJarClass(jarEntryName: String, classBytes: ByteArray?): ByteArray? {
        val classReader = ClassReader(classBytes)
        if (filter(jarEntryName)) {
            return null
        }
        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
        val classVisitor = CustomClassVisitor(classWriter, classReader)
        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
        return classWriter.toByteArray()
    }

    /**
     * transform class
     */
    private fun transformClass(classFile: File?, tempDir: File): File? {
        try {
            if (classFile == null) return null
            var changeFile: File? = null
            val classReader = ClassReader(IOGroovyMethods.getBytes(FileInputStream(classFile)))
            if (filter(classFile.name)) {
                return null
            }
            val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
            val classVisitor = CustomClassVisitor(classWriter, classReader)
            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
            val changeClassBytes = classWriter.toByteArray();
            if (changeClassBytes.isNotEmpty()) {
                val className = classReader.className.replace("/", ".").replace(".class", "")
                changeFile = File(tempDir, className.replace(".", "") + ".class")
                if (changeFile.exists()) {
                    changeFile.delete()
                }
                changeFile.createNewFile()
                FileUtils.writeByteArrayToFile(changeFile, changeClassBytes)
            }
            return classFile
        } catch (ex: Exception) {
            println(ex.message)
            return null
        }
    }

    private fun filter(className: String?): Boolean {
        if (className == null) return true
        val realClassName = className.split("/").last().replace(".class", "")
        println(realClassName)
        return realClassName.startsWith("R$")
                || realClassName == "R"
                || realClassName == "BuildConfig"
    }

    inner class CustomClassVisitor(val classWriter: ClassWriter, val classReader: ClassReader) :
        ClassVisitor(Opcodes.ASM7, classWriter) {
        private val superName = classReader.superName
        private val methodNameList = mutableListOf<String>()

        override fun visitMethod(
            access: Int,
            name: String?,
            descriptor: String?,
            signature: String?,
            exceptions: Array<out String>?
        ): MethodVisitor {
            if (name == null || descriptor == null) return super.visitMethod(
                access,
                name,
                descriptor,
                signature,
                exceptions
            )
            val isClickMethod = ClickMethodUtils.isViewClickMethod(methodNameList, name, descriptor, superName)
            methodNameList.add(name)
            val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
            return CustomMethodVisitor(classReader, methodVisitor, isClickMethod, access, name, descriptor)
        }


    }

    private fun fileTraverse(file: File, filter: (file: File, name: String) -> Boolean, accept: (file: File) -> Unit) {
        file.listFiles()!!.forEach { childFile ->
            if (childFile.isDirectory) {
                fileTraverse(childFile, filter, accept)
            } else {
                if (filter.invoke(childFile, childFile.name)) {
                    accept.invoke(childFile)
                }
            }
        }
    }
}